"""
The classic game of flappy bird. Make with python
and pygame. Features pixel perfect collision using masks :o

Date Modified:  Jul 30, 2019
Author: Tech With Tim
Estimated Work Time: 5 hours (1 just for that damn collision)
"""
import pygame # pygame包
import random # 随机数
import os # 系统
import time # 时间
import neat # 遗传拓扑神经网络
import visualize # 可视化
import pickle # 模块pickle 实现了对一个 Python 对象结构的二进制序列化和反序列化。
pygame.font.init()  # init font

WIN_WIDTH = 600 # 窗口宽度
WIN_HEIGHT = 800 # 窗口高度
FLOOR = 730 #地面高度
STAT_FONT = pygame.font.SysFont("comicsans", 50) #？
END_FONT = pygame.font.SysFont("comicsans", 70) #？
DRAW_LINES = True # 画出鸟和管子之间的线

WIN = pygame.display.set_mode((WIN_WIDTH, WIN_HEIGHT)) #pygame的窗口制作
pygame.display.set_caption("Flappy Bird") #pygame的窗口标题

pipe_img = pygame.transform.scale2x(pygame.image.load(os.path.join("imgs","pipe.png")).convert_alpha()) # 管子的图片
bg_img = pygame.transform.scale(pygame.image.load(os.path.join("imgs","bg.png")).convert_alpha(), (600, 900)) # 背景图片
bird_images = [pygame.transform.scale2x(pygame.image.load(os.path.join("imgs","bird" + str(x) + ".png"))) for x in range(1,4)] # 小鸟图片
base_img = pygame.transform.scale2x(pygame.image.load(os.path.join("imgs","base.png")).convert_alpha()) # 底部图片

gen = 0 #代数

class Bird:
    """
    Bird class representing the flappy bird
    """
    MAX_ROTATION = 25 # 最大转动
    IMGS = bird_images # 图片
    ROT_VEL = 20 #
    ANIMATION_TIME = 5 #动画时间

    def __init__(self, x, y): # 初始化
        """
        Initialize the object 初始化对象
        :param x: starting x pos (int) 开始的X位置
        :param y: starting y pos (int) 开始的Y位置
        :return: None 没有返回值
        """
        self.x = x #自己的X值
        self.y = y #自己的Y值
        self.tilt = 0  # degrees to tilt #倾斜度
        self.tick_count = 0 #滴答计数
        self.vel = 0 #
        self.height = self.y #高度就是Y
        self.img_count = 0 #图片计数
        self.img = self.IMGS[0] #图片

    def jump(self): #跳
        """
        make the bird jump 让鸟跳
        :return: None 没有返回值
        """
        self.vel = -10.5 #
        self.tick_count = 0 #滴答计数
        self.height = self.y #高度

    def move(self): #移动
        """
        make the bird move 让鸟移动
        :return: None 没有返回值
        """
        self.tick_count += 1 #滴答计数+1

        # for downward acceleration 下落加速
        displacement = self.vel*(self.tick_count) + 0.5*(3)*(self.tick_count)**2  # calculate displacement 计算替换位置
        # 替换位置 = vel*滴答计数 + 0.5*（3）*滴答计数^2
        # 包含下落加速

        # terminal velocity 最终速率
        if displacement >= 16: #如果替换位置大于16，最终等于16
            displacement = (displacement/abs(displacement)) * 16

        if displacement < 0: #如果替换位置小于0
            displacement -= 2 #替换位置-2

        self.y = self.y + displacement #更新Y位置

        if displacement < 0 or self.y < self.height + 50:  # tilt up 斜向上
            #替换位置小于0 或 y大于高度+50
            if self.tilt < self.MAX_ROTATION: #自己的倾斜小于 自己的最大转动
                self.tilt = self.MAX_ROTATION #那么自己的倾斜等于自己的最大转融
        else:  # tilt down 斜向下
            if self.tilt > -90: #倾斜大于-90度
                self.tilt -= self.ROT_VEL #倾斜减去 rot

    def draw(self, win): #画出来
        """
        draw the bird 画出小鸟
        :param win: pygame window or surface 窗口
        :return: None 没有返回值
        """
        self.img_count += 1 #图片计数+1

        # For animation of bird, loop through three images
        # 动画小鸟，循环3张图片
        if self.img_count <= self.ANIMATION_TIME:
            self.img = self.IMGS[0]
        elif self.img_count <= self.ANIMATION_TIME*2:
            self.img = self.IMGS[1]
        elif self.img_count <= self.ANIMATION_TIME*3:
            self.img = self.IMGS[2]
        elif self.img_count <= self.ANIMATION_TIME*4:
            self.img = self.IMGS[1]
        elif self.img_count == self.ANIMATION_TIME*4 + 1:
            self.img = self.IMGS[0]
            self.img_count = 0

        # so when bird is nose diving it isn't flapping
        # 当小鸟大头朝下时，就不再扑棱
        if self.tilt <= -80:#倾斜度小于-80
            self.img = self.IMGS[1]#只播放一张图片
            self.img_count = self.ANIMATION_TIME*2


        # tilt the bird 倾斜小鸟
        blitRotateCenter(win, self.img, (self.x, self.y), self.tilt)

    def get_mask(self):
        """
        gets the mask for the current image of the bird 取小鸟的遮罩
        :return: None 没有返回值
        """
        return pygame.mask.from_surface(self.img)


class Pipe():
    """
    represents a pipe object 管子对象
    """
    GAP = 150 #缺口
    # 130 1915代都过不去
    # 140 也过不去
    VEL = 5 #

    def __init__(self, x):
        """
        initialize pipe object 初始化管子对象
        :param x: int
        :param y: int 没有Y啊
        :return" None
        """
        self.x = x # X等于X
        self.height = 0 #高度设为0

        # where the top and bottom of the pipe is
        # 管子的顶和底
        self.top = 0 #顶设为0
        self.bottom = 0 #底设为0

        self.PIPE_TOP = pygame.transform.flip(pipe_img, False, True) # 顶的图片
        self.PIPE_BOTTOM = pipe_img # 底的图片

        self.passed = False #穿过的

        self.set_height() #设置高度

    def set_height(self):#设置高度函数
        """
        set the height of the pipe, from the top of the screen 设置管子的高度，从顶部屏幕往下
        :return: None 没有返回值
        """
        self.height = random.randrange(10, 650) #随机一个从50到450的值
        self.top = self.height - self.PIPE_TOP.get_height() #顶部等于 高度-顶部图片的高度
        self.bottom = self.height + self.GAP #底部等于 高度+缺口
        #顶部指的是顶部的管子，底部指的是底部的管子

    def move(self):
        """
        move pipe based on vel 管子的移动
        :return: None 没有返回值
        """
        self.x -= self.VEL #X减去一个值

    def draw(self, win):
        """
        draw both the top and bottom of the pipe 同时画顶部和底部的管子
        :param win: pygame window/surface 参数窗口
        :return: None 没有返回值
        """
        # draw top 画顶部管子
        win.blit(self.PIPE_TOP, (self.x, self.top))
        # draw bottom 画底部管子
        win.blit(self.PIPE_BOTTOM, (self.x, self.bottom))


    def collide(self, bird, win):
        """
        returns if a point is colliding with the pipe 返回，如果一个点撞击到了管子
        :param bird: Bird object 参数传进去小鸟
        :return: Bool 返回一个布尔值
        """
        bird_mask = bird.get_mask() #小鸟的遮罩
        top_mask = pygame.mask.from_surface(self.PIPE_TOP) #管子的顶部遮罩
        bottom_mask = pygame.mask.from_surface(self.PIPE_BOTTOM) #管子的底部遮罩
        top_offset = (self.x - bird.x, self.top - round(bird.y)) #顶部偏移值
        bottom_offset = (self.x - bird.x, self.bottom - round(bird.y)) #底部偏移量

        b_point = bird_mask.overlap(bottom_mask, bottom_offset) #底部的点
        t_point = bird_mask.overlap(top_mask,top_offset) #顶部的点
        #这两个值应该就是

        if b_point or t_point: #如果B或者T，那么就返回真了
            return True

        return False

class Base:# 基底
    """
    Represnts the moving floor of the game
    移动的底部
    """
    VEL = 5 # 这个可能是速度
    WIDTH = base_img.get_width() #宽度=图片宽度
    IMG = base_img # 图片

    def __init__(self, y):
        """
        Initialize the object 初始化对象
        :param y: int 参数为y
        :return: None 没有返回值
        """
        self.y = y #设置Y
        self.x1 = 0 #设置X1
        self.x2 = self.WIDTH # 设置X2

    def move(self):
        """
        move floor so it looks like its scrolling 移动地板让他看起来像在滚动
        :return: None
        """
        self.x1 -= self.VEL
        self.x2 -= self.VEL
        if self.x1 + self.WIDTH < 0:
            self.x1 = self.x2 + self.WIDTH

        if self.x2 + self.WIDTH < 0:
            self.x2 = self.x1 + self.WIDTH

    def draw(self, win):
        """
        Draw the floor. This is two images that move together. 画出地板
        :param win: the pygame surface/window
        :return: None 没有返回值
        """
        win.blit(self.IMG, (self.x1, self.y))
        win.blit(self.IMG, (self.x2, self.y))


def blitRotateCenter(surf, image, topleft, angle):
    """
    Rotate a surface and blit it to the window
    旋转一个界面然后位块传输给窗口
    :param surf: the surface to blit to 参数：界面
    :param image: the image surface to rotate 参数：图像
    :param topLeft: the top left position of the image 图像的顶部和左部
    :param angle: a float value for angle 角度的浮点数
    :return: None 没有返回值
    """
    rotated_image = pygame.transform.rotate(image, angle) #转动图像
    new_rect = rotated_image.get_rect(center = image.get_rect(topleft = topleft).center) #新矩形
    surf.blit(rotated_image, new_rect.topleft) #位块创术

def draw_window(win, birds, pipes, base, score, gen, pipe_ind):
    """
    draws the windows for the main game loop
    画主游戏循环的窗口
    :param win: pygame window surface 参数：窗口-pygame窗口界面
    :param bird: a Bird object 参数：小鸟
    :param pipes: List of pipes 参数：管子（list）
    :param score: score of the game (int) 参数：分数
    :param gen: current generation 参数：代数
    :param pipe_ind: index of closest pipe 参数：管子索引，最近的管子的索引
    :return: None 没有返回值
    """
    if gen == 0:# 代数等于0
        gen = 1# 代数等于1
    win.blit(bg_img, (0,0))#窗口背景图片

    for pipe in pipes:
        pipe.draw(win)# 在窗口上画管子

    base.draw(win)# 基底画在窗口上
    for bird in birds:# 小鸟 从 鸟群
        # draw lines from bird to pipe 画线从小鸟到管子
        if DRAW_LINES: #画线
            try:
                pygame.draw.line(win, (255,0,0), (bird.x+bird.img.get_width()/2, bird.y + bird.img.get_height()/2), (pipes[pipe_ind].x + pipes[pipe_ind].PIPE_TOP.get_width()/2, pipes[pipe_ind].height), 5)
                pygame.draw.line(win, (255,0,0), (bird.x+bird.img.get_width()/2, bird.y + bird.img.get_height()/2), (pipes[pipe_ind].x + pipes[pipe_ind].PIPE_BOTTOM.get_width()/2, pipes[pipe_ind].bottom), 5)
            except:
                pass
        # draw bird
        bird.draw(win)#画这个线

    # score 分数
    score_label = STAT_FONT.render("Score: " + str(score),1,(255,255,255)) #分数标签
    win.blit(score_label, (WIN_WIDTH - score_label.get_width() - 15, 10)) #画在窗口上

    # generations 代数
    score_label = STAT_FONT.render("Gens: " + str(gen-1),1,(255,255,255)) #代数标签
    win.blit(score_label, (10, 10)) #画在窗口上

    # alive 存活数
    score_label = STAT_FONT.render("Alive: " + str(len(birds)),1,(255,255,255)) #存活数标签
    win.blit(score_label, (10, 50)) #画在窗口上

    pygame.display.update() #显示更新


def eval_genomes(genomes, config):#评估基因
    """
    runs the simulation of the current population of
    birds and sets their fitness based on the distance they
    reach in the game.
    """
    global WIN, gen
    win = WIN #窗口
    gen += 1 #代数

    # start by creating lists holding the genome itself, the
    # 启动建立列表库 获得基因自己
    # neural network associated with the genome and the
    # 神经网络协助基因和小鸟对象
    # bird object that uses that network to play
    # 用网络来玩（游戏）

    nets = [] #神经网络对象
    birds = [] #小鸟（复数）对象
    ge = [] #代数
    for genome_id, genome in genomes: #基因库
        genome.fitness = 0  # start with fitness level of 0 从适应度0开始
        net = neat.nn.FeedForwardNetwork.create(genome, config) # 创建NEAT（前向网络）
        nets.append(net) #Net List 里增加一个 net
        birds.append(Bird(230,350)) #增加一个小鸟
        ge.append(genome) #增加一代

    base = Base(FLOOR)  #地板
    pipes = [Pipe(700)] #管子
    score = 0 #分数

    clock = pygame.time.Clock() #时间

    run = True
    while run and len(birds) > 0:
        clock.tick(3000)#数字越大速度越快
        #初始设置的是30

        for event in pygame.event.get():#响应事件值守循环
            if event.type == pygame.QUIT:#退出事件
                run = False
                pygame.quit()
                quit()
                break

        pipe_ind = 0 #第0个管道
        if len(birds) > 0: #鸟大于0个
            if len(pipes) > 1 and birds[0].x > pipes[0].x + pipes[0].PIPE_TOP.get_width():  # determine whether to use the first or second
                pipe_ind = 1                                                                 # pipe on the screen for neural network input

        for x, bird in enumerate(birds):  # give each bird a fitness of 0.1 for each frame it stays alive
            ge[x].fitness += 0.1
            bird.move()

            # send bird location, top pipe location and bottom pipe location and determine from network whether to jump or not
            output = nets[birds.index(bird)].activate((bird.y, abs(bird.y - pipes[pipe_ind].height), abs(bird.y - pipes[pipe_ind].bottom)))

            if output[0] > 0.5:  # we use a tanh activation function so result will be between -1 and 1. if over 0.5 jump
                bird.jump()

        base.move()

        rem = []
        add_pipe = False
        for pipe in pipes:
            pipe.move()
            # check for collision
            for bird in birds:
                if pipe.collide(bird, win):
                    ge[birds.index(bird)].fitness -= 1
                    nets.pop(birds.index(bird))
                    ge.pop(birds.index(bird))
                    birds.pop(birds.index(bird))

            if pipe.x + pipe.PIPE_TOP.get_width() < 0:
                rem.append(pipe)

            if not pipe.passed and pipe.x < bird.x:
                pipe.passed = True
                add_pipe = True

        if add_pipe:
            score += 1
            # can add this line to give more reward for passing through a pipe (not required)
            for genome in ge:
                genome.fitness += 5
            pipes.append(Pipe(WIN_WIDTH))

        for r in rem:
            pipes.remove(r)

        for bird in birds:
            if bird.y + bird.img.get_height() - 10 >= FLOOR or bird.y < -50:
                nets.pop(birds.index(bird))
                ge.pop(birds.index(bird))
                birds.pop(birds.index(bird))

        draw_window(WIN, birds, pipes, base, score, gen, pipe_ind)

        # break if score gets large enough
        '''if score > 20:
            pickle.dump(nets[0],open("best.pickle", "wb"))
            break'''


def run(config_file):
    """
    runs the NEAT algorithm to train a neural network to play flappy bird.
    运行NEAT算法去训练一个神经网络去玩扑棱的小鸟
    :param config_file: location of config file
    参数配置的文件
    :return: None 没有返回值
    """
    config = neat.config.Config(neat.DefaultGenome, neat.DefaultReproduction,
                         neat.DefaultSpeciesSet, neat.DefaultStagnation,
                         config_file)

    # Create the population, which is the top-level object for a NEAT run.
    p = neat.Population(config)

    # Add a stdout reporter to show progress in the terminal.
    p.add_reporter(neat.StdOutReporter(True))
    stats = neat.StatisticsReporter()
    p.add_reporter(stats)
    #p.add_reporter(neat.Checkpointer(5))

    # Run for up to 50 generations.
    winner = p.run(eval_genomes, 5000000)

    # show final stats
    print('\nBest genome:\n{!s}'.format(winner))


if __name__ == '__main__': #
    # Determine path to configuration file. This path manipulation is
    # here so that the script will run successfully regardless of the
    # current working directory.
    local_dir = os.path.dirname(__file__)
    config_path = os.path.join(local_dir, 'config-feedforward.txt')
    run(config_path)
